You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
897 lines
30 KiB
897 lines
30 KiB
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { DotsLoader } from "@/components/Componentes/button";
|
|
import NavigationButton from "@/components/Componentes/navigation-button";
|
|
import { PageBackground } from "@/components/Componentes/page-background";
|
|
import {
|
|
hasQuestionAnswerValue,
|
|
QuestionAnswersProvider,
|
|
useQuestionAnswers,
|
|
} from "@/components/Componentes/question-answer-storage";
|
|
import QuestionExitNavigationButton from "@/components/Componentes/question-exit-navigation-button";
|
|
import QuestionRenderer from "@/components/Componentes/question-renderer";
|
|
import QuestionSectionFlow from "@/components/Componentes/question-section-flow";
|
|
import StickyHeader from "@/components/Componentes/sticky-header";
|
|
import TestIntroPage from "@/components/Componentes/test-intro-page";
|
|
import TestQuestionsFlow, {
|
|
type TestQuestion,
|
|
} from "@/components/Componentes/test-questions-flow";
|
|
import { cattellFallbackQuestions } from "@/data/cattell-fallback";
|
|
import { glasserFallbackQuestions } from "@/data/glasser-fallback";
|
|
import {
|
|
getQuestionListItemBySlug,
|
|
isQuestionListItemVisibleForProfile,
|
|
isQuestionRequiredForProfile,
|
|
isQuestionVisibleForProfile,
|
|
type QuestionField,
|
|
} from "@/data/question-data";
|
|
import type { MarriageGender } from "@/hooks/marriage/types";
|
|
import {
|
|
useCattellQuestionsQuery,
|
|
useSubmitCattellAssessmentMutation,
|
|
} from "@/hooks/marriage/use-cattell";
|
|
import {
|
|
useGlasserQuestionsQuery,
|
|
useSubmitGlasserAssessmentMutation,
|
|
} from "@/hooks/marriage/use-glasser";
|
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
|
|
import { defaultLocale, type Locale } from "@/translations/config";
|
|
import { useI18n } from "@/translations/provider";
|
|
import AnswerPaceSheet from "./answer-pace-sheet";
|
|
|
|
type QuestionDetailClientProps = {
|
|
closeLabel: string;
|
|
continueLabel: string;
|
|
description: string;
|
|
informationLabel: string;
|
|
itemSlug: string;
|
|
locale?: Locale;
|
|
questionsListHref: string;
|
|
title: string;
|
|
};
|
|
|
|
type StoredQuestionField = {
|
|
label?: string;
|
|
value?: unknown;
|
|
type?: string;
|
|
key?: string;
|
|
};
|
|
|
|
type StoredAnswers = {
|
|
fields?: StoredQuestionField[];
|
|
};
|
|
|
|
function getQuestionStorageKey(slug: string) {
|
|
return `marriage:sections:${slug}:answers`;
|
|
}
|
|
|
|
function parseStoredAge(value: unknown) {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
const trimmedValue = value.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return null;
|
|
}
|
|
|
|
const numericAge = Number(trimmedValue);
|
|
|
|
if (Number.isFinite(numericAge)) {
|
|
return numericAge;
|
|
}
|
|
|
|
const dateOfBirth = new Date(trimmedValue);
|
|
|
|
if (Number.isNaN(dateOfBirth.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
const today = new Date();
|
|
let age = today.getFullYear() - dateOfBirth.getFullYear();
|
|
const hasBirthdayPassed =
|
|
today.getMonth() > dateOfBirth.getMonth() ||
|
|
(today.getMonth() === dateOfBirth.getMonth() &&
|
|
today.getDate() >= dateOfBirth.getDate());
|
|
|
|
if (!hasBirthdayPassed) {
|
|
age -= 1;
|
|
}
|
|
|
|
return age >= 0 ? age : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getStoredAge() {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(
|
|
getQuestionStorageKey("personal_info"),
|
|
);
|
|
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
|
|
const storedAnswers = JSON.parse(rawValue) as StoredAnswers;
|
|
const ageField = storedAnswers.fields?.find((field) => {
|
|
const f = field as { key?: string; type?: string; label?: string };
|
|
return (
|
|
f.type === "number" ||
|
|
f.label === "Age" ||
|
|
f.label === "سن" ||
|
|
(typeof f.key === "string" &&
|
|
(f.key.endsWith("_age") || f.key.endsWith("_sn")))
|
|
);
|
|
});
|
|
|
|
if (ageField) {
|
|
return parseStoredAge(ageField.value);
|
|
}
|
|
|
|
const dateOfBirthField = storedAnswers.fields?.find((field) => {
|
|
const f = field as { key?: string; type?: string; label?: string };
|
|
return (
|
|
f.type === "date" ||
|
|
f.label === "Date of Birth" ||
|
|
f.label === "تاریخ تولد" ||
|
|
(typeof f.key === "string" &&
|
|
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld")))
|
|
);
|
|
});
|
|
|
|
return parseStoredAge(dateOfBirthField?.value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function QuestionFlowWrapper({
|
|
visibleQuestions,
|
|
itemSlug,
|
|
dobQuestion,
|
|
dobQuestionIndex,
|
|
continueLabel,
|
|
questionsListHref,
|
|
}: {
|
|
visibleQuestions: QuestionField[];
|
|
itemSlug: string;
|
|
dobQuestion?: QuestionField;
|
|
dobQuestionIndex?: number;
|
|
requiredQuestionsCount: number;
|
|
continueLabel: string;
|
|
questionsListHref: string;
|
|
}) {
|
|
const { getAnswerValue } = useQuestionAnswers();
|
|
|
|
const dynamicQuestions = useMemo(() => {
|
|
// Find employment status question by title or by choices length (10)
|
|
const employmentQuestionIndex = visibleQuestions.findIndex((q) => {
|
|
return (
|
|
q.title === "Employment Status" ||
|
|
q.title === "وضعیت اشتغال" ||
|
|
(q.type === "dropdown" && q.extras.options?.length === 10)
|
|
);
|
|
});
|
|
|
|
let employmentSelectedOptionIndex = -1;
|
|
if (employmentQuestionIndex !== -1) {
|
|
const employmentQuestion = visibleQuestions[employmentQuestionIndex];
|
|
const ans = getAnswerValue(employmentQuestion, employmentQuestionIndex);
|
|
if (ans) {
|
|
employmentSelectedOptionIndex =
|
|
employmentQuestion.extras.options?.indexOf(String(ans)) ?? -1;
|
|
}
|
|
}
|
|
|
|
// Find Parents' Survival Status question index and check selected index
|
|
const survivalStatusQuestionIndex = visibleQuestions.findIndex((q) => {
|
|
return (
|
|
q.title === "Parents' Survival Status" ||
|
|
q.title === "وضعیت حیات والدین"
|
|
);
|
|
});
|
|
|
|
let survivalSelectedOptionIndex = -1;
|
|
if (survivalStatusQuestionIndex !== -1) {
|
|
const survivalQuestion = visibleQuestions[survivalStatusQuestionIndex];
|
|
const ans = getAnswerValue(survivalQuestion, survivalStatusQuestionIndex);
|
|
if (ans) {
|
|
survivalSelectedOptionIndex =
|
|
survivalQuestion.extras.options?.indexOf(String(ans)) ?? -1;
|
|
}
|
|
}
|
|
|
|
// Find Parents' Marital Status question index and check selected index
|
|
const maritalStatusQuestionIndex = visibleQuestions.findIndex((q) => {
|
|
return (
|
|
q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین"
|
|
);
|
|
});
|
|
|
|
let isCircumstancesSelected = false;
|
|
if (maritalStatusQuestionIndex !== -1) {
|
|
const maritalQuestion = visibleQuestions[maritalStatusQuestionIndex];
|
|
const ans = getAnswerValue(maritalQuestion, maritalStatusQuestionIndex);
|
|
if (ans) {
|
|
const selectedIdx =
|
|
maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1;
|
|
isCircumstancesSelected = selectedIdx === 2;
|
|
}
|
|
}
|
|
|
|
// Find Current Marital Status question index and check selected index
|
|
const currentMaritalQuestionIndex = visibleQuestions.findIndex((q) => {
|
|
return (
|
|
q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی"
|
|
);
|
|
});
|
|
|
|
let maritalSelectedIndex = -1;
|
|
if (currentMaritalQuestionIndex !== -1) {
|
|
const maritalQuestion = visibleQuestions[currentMaritalQuestionIndex];
|
|
const ans = getAnswerValue(maritalQuestion, currentMaritalQuestionIndex);
|
|
if (ans) {
|
|
maritalSelectedIndex =
|
|
maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1;
|
|
}
|
|
}
|
|
|
|
// Find Children and Guardianship Status question index
|
|
const custodyQuestionIndex = visibleQuestions.findIndex((q) => {
|
|
return (
|
|
q.title === "Children and Guardianship Status" ||
|
|
q.title === "وضعیت فرزند و تکفل"
|
|
);
|
|
});
|
|
|
|
let hasChildrenSelected = false;
|
|
let hasAnyGuardianshipSelected = false;
|
|
|
|
if (custodyQuestionIndex !== -1) {
|
|
const custodyQuestion = visibleQuestions[custodyQuestionIndex];
|
|
const ans = getAnswerValue(custodyQuestion, custodyQuestionIndex);
|
|
if (ans) {
|
|
const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)];
|
|
hasChildrenSelected = ansList.some(
|
|
(val) => val.includes("Have children") || val.includes("فرزند دارم"),
|
|
);
|
|
hasAnyGuardianshipSelected = ansList.some(
|
|
(val) =>
|
|
val.includes("Have children") ||
|
|
val.includes("فرزند دارم") ||
|
|
val.includes("under my guardianship") ||
|
|
val.includes("تحت تکفل"),
|
|
);
|
|
}
|
|
}
|
|
|
|
const filtered = visibleQuestions.filter((question, index) => {
|
|
// Check if this is one of the marital status/children questions
|
|
if (currentMaritalQuestionIndex !== -1) {
|
|
const isDuration =
|
|
index === currentMaritalQuestionIndex + 1 ||
|
|
question.title === "Previous Marriage Duration" ||
|
|
question.title === "مدت ازدواج یا عقد قبلی";
|
|
|
|
const isSeparation =
|
|
index === currentMaritalQuestionIndex + 2 ||
|
|
question.title === "Reason for Separation" ||
|
|
question.title === "علت جدایی، در صورت وجود";
|
|
|
|
const isCustody =
|
|
index === currentMaritalQuestionIndex + 3 ||
|
|
question.title === "Children and Guardianship Status" ||
|
|
question.title === "وضعیت فرزند و تکفل";
|
|
|
|
const isChildrenCount =
|
|
index === currentMaritalQuestionIndex + 4 ||
|
|
question.title === "Number of Children" ||
|
|
question.title === "تعداد فرزندان";
|
|
|
|
const isChildrenExplanation =
|
|
index === currentMaritalQuestionIndex + 5 ||
|
|
question.title === "Short Children/Guardianship Explanation" ||
|
|
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل";
|
|
|
|
if (isDuration) {
|
|
return [1, 2, 3].includes(maritalSelectedIndex);
|
|
}
|
|
if (isSeparation) {
|
|
return [1, 2].includes(maritalSelectedIndex);
|
|
}
|
|
if (isCustody) {
|
|
return [2, 3].includes(maritalSelectedIndex);
|
|
}
|
|
if (isChildrenCount) {
|
|
return [2, 3].includes(maritalSelectedIndex) && hasChildrenSelected;
|
|
}
|
|
if (isChildrenExplanation) {
|
|
return (
|
|
[2, 3].includes(maritalSelectedIndex) && hasAnyGuardianshipSelected
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check if this is one of the three job-related questions
|
|
if (employmentQuestionIndex !== -1) {
|
|
const isJobTitle =
|
|
index === employmentQuestionIndex + 1 ||
|
|
question.title === "Job Title" ||
|
|
question.title === "عنوان شغلی";
|
|
|
|
const isWorkLocation =
|
|
index === employmentQuestionIndex + 2 ||
|
|
question.title === "Work Location" ||
|
|
question.title === "محل فعالیت";
|
|
|
|
const isMonthlyIncome =
|
|
index === employmentQuestionIndex + 3 ||
|
|
question.title === "Monthly Income" ||
|
|
question.title === "میزان درآمد ماهانه";
|
|
|
|
if (isJobTitle || isWorkLocation || isMonthlyIncome) {
|
|
// If no employment status is selected yet, hide them by default
|
|
if (employmentSelectedOptionIndex === -1) {
|
|
return false;
|
|
}
|
|
|
|
// Options logic:
|
|
// Show all 3 for: index 0 (Full-time), 1 (Part-time), 2 (Self-employed), 3 (Entrepreneur), 5 (Working Student)
|
|
const showAll = [0, 1, 2, 3, 5].includes(
|
|
employmentSelectedOptionIndex,
|
|
);
|
|
// Hide all 3 for: index 4 (Student), 6 (Student & Job Seeking), 7 (Job Seeking / Unemployed), 8 (Homemaker)
|
|
const hideAll = [4, 6, 7, 8].includes(employmentSelectedOptionIndex);
|
|
// Special Retired logic: index 9 (Retired)
|
|
const isRetired = employmentSelectedOptionIndex === 9;
|
|
|
|
if (showAll) {
|
|
return true;
|
|
}
|
|
if (hideAll) {
|
|
return false;
|
|
}
|
|
if (isRetired) {
|
|
if (isJobTitle || isMonthlyIncome) {
|
|
return true;
|
|
}
|
|
if (isWorkLocation) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check Parents' Survival Status to decide if Parents' Marital Status is visible
|
|
if (survivalStatusQuestionIndex !== -1) {
|
|
const isParentsMaritalStatus =
|
|
question.title === "Parents' Marital Status" ||
|
|
question.title === "وضعیت تأهل والدین";
|
|
|
|
if (isParentsMaritalStatus) {
|
|
return survivalSelectedOptionIndex === 0;
|
|
}
|
|
}
|
|
|
|
// Default dependsOn logic
|
|
if (question.logic?.dependsOn) {
|
|
const { title, values } = question.logic.dependsOn;
|
|
const dependentQuestionIndex = visibleQuestions.findIndex(
|
|
(q) => q.title === title,
|
|
);
|
|
|
|
if (dependentQuestionIndex !== -1) {
|
|
const dependentQuestion = visibleQuestions[dependentQuestionIndex];
|
|
const answer = getAnswerValue(
|
|
dependentQuestion,
|
|
dependentQuestionIndex,
|
|
);
|
|
if (Array.isArray(answer)) {
|
|
return answer.some((ans) => values.includes(String(ans)));
|
|
}
|
|
return values.includes(String(answer));
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return filtered.map((question) => {
|
|
if (
|
|
question.title === "Short Family Description" ||
|
|
question.title === "توضیح کوتاه درباره خانواده"
|
|
) {
|
|
return {
|
|
...question,
|
|
required: isCircumstancesSelected,
|
|
};
|
|
}
|
|
|
|
if (
|
|
question.title === "Previous Marriage Duration" ||
|
|
question.title === "مدت ازدواج یا عقد قبلی" ||
|
|
question.title === "Number of Children" ||
|
|
question.title === "تعداد فرزندان" ||
|
|
question.title === "Short Children/Guardianship Explanation" ||
|
|
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل" ||
|
|
question.title === "Additional details about family responsibility" ||
|
|
question.title === "توضیحات تکمیلی درباره مسئولیت خانوادگی" ||
|
|
question.title === "Do the supported individual(s) live with you?" ||
|
|
question.title === "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟" ||
|
|
question.title === "What is the custody status of your child(ren)?" ||
|
|
question.title === "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟" ||
|
|
question.title ===
|
|
"Does the custody, visitation, or relocation schedule impact your residence or immigration?" ||
|
|
question.title ===
|
|
"آیا برنامه حضانت، ملاقات یا جابهجایی فرزند بر محل زندگی یا امکان مهاجرت شما تأثیر میگذارد؟" ||
|
|
question.title ===
|
|
"What is the payment or receipt status of child support?" ||
|
|
question.title ===
|
|
"وضعیت پرداخت یا دریافت نفقه و حمایت مالی فرزند چگونه است؟" ||
|
|
question.title ===
|
|
"Acceptance of necessary communication between future spouse and the other parent" ||
|
|
question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگرِ فرزند" ||
|
|
question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگر فرزند"
|
|
) {
|
|
return {
|
|
...question,
|
|
required: true,
|
|
};
|
|
}
|
|
return question;
|
|
});
|
|
}, [visibleQuestions, getAnswerValue]);
|
|
|
|
const requiredCount = useMemo(
|
|
() => dynamicQuestions.filter((q) => q.required).length,
|
|
[dynamicQuestions],
|
|
);
|
|
|
|
return (
|
|
<QuestionSectionFlow
|
|
key={itemSlug}
|
|
total={requiredCount}
|
|
continueLabel={continueLabel}
|
|
exitHref={questionsListHref}
|
|
optionalQuestionIndexes={dynamicQuestions.flatMap((question, index) =>
|
|
question.required ? [] : [index],
|
|
)}
|
|
questions={dynamicQuestions}
|
|
>
|
|
{dynamicQuestions.map((question, index) => {
|
|
let originalIndex = visibleQuestions.indexOf(question);
|
|
if (originalIndex === -1) {
|
|
// Spread-copied questions lose reference equality; fall back to title
|
|
originalIndex = visibleQuestions.findIndex(
|
|
(q) => q.title === question.title,
|
|
);
|
|
}
|
|
const answer = getAnswerValue(question, originalIndex);
|
|
const hasAnswer = hasQuestionAnswerValue(answer ?? null);
|
|
let isAnswered = hasAnswer;
|
|
|
|
if (hasAnswer) {
|
|
const isEmailQuestion =
|
|
question.title.toLowerCase().includes("email") ||
|
|
question.title.includes("ایمیل");
|
|
if (isEmailQuestion) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
isAnswered = emailRegex.test(String(answer).trim());
|
|
} else if (question.type === "birthplace") {
|
|
const strVal = String(answer);
|
|
const parts = strVal.split(",").map((p) => p.trim());
|
|
isAnswered =
|
|
parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0;
|
|
} else if (question.type === "checkbox") {
|
|
isAnswered = Array.isArray(answer) ? answer.length > 0 : hasAnswer;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={`${itemSlug}-${question.title}`}
|
|
data-question-required={String(question.required)}
|
|
data-question-optional={String(!question.required)}
|
|
data-question-index={index}
|
|
data-question-original-index={originalIndex}
|
|
data-question-disabled="false"
|
|
data-question-answered={String(isAnswered)}
|
|
>
|
|
<QuestionRenderer
|
|
question={question}
|
|
questionIndex={originalIndex}
|
|
dobQuestion={dobQuestion}
|
|
dobQuestionIndex={dobQuestionIndex}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</QuestionSectionFlow>
|
|
);
|
|
}
|
|
|
|
export default function QuestionDetailClient({
|
|
closeLabel,
|
|
continueLabel,
|
|
description,
|
|
informationLabel,
|
|
itemSlug,
|
|
locale = defaultLocale,
|
|
questionsListHref,
|
|
title,
|
|
}: QuestionDetailClientProps) {
|
|
const router = useRouter();
|
|
const { dictionary: t } = useI18n();
|
|
const [isTestStarted, setIsTestStarted] = useState(false);
|
|
const { data: profile, isLoading: isProfileLoading } =
|
|
useMarriageProfileQuery();
|
|
const profileGender = profile?.gender;
|
|
const age = getStoredAge();
|
|
const item = getQuestionListItemBySlug(itemSlug, locale);
|
|
|
|
const isCattellSlug = itemSlug === "personality_test";
|
|
const isGlasserSlug = itemSlug === "glasser_5_needs_test";
|
|
|
|
const cattellQuery = useCattellQuestionsQuery(locale, {
|
|
enabled: isCattellSlug && isTestStarted,
|
|
retry: 0,
|
|
});
|
|
const submitCattellMutation = useSubmitCattellAssessmentMutation();
|
|
|
|
const glasserQuery = useGlasserQuestionsQuery(locale, {
|
|
enabled: isGlasserSlug && isTestStarted,
|
|
retry: 0,
|
|
});
|
|
const submitGlasserMutation = useSubmitGlasserAssessmentMutation();
|
|
|
|
const profileContext = useMemo(
|
|
() => ({
|
|
age,
|
|
gender: profileGender as MarriageGender | null | undefined,
|
|
}),
|
|
[age, profileGender],
|
|
);
|
|
|
|
const cattellTestQuestions: TestQuestion[] = useMemo(() => {
|
|
const questionsList =
|
|
cattellQuery.data?.questions && cattellQuery.data.questions.length > 0
|
|
? cattellQuery.data.questions
|
|
: cattellFallbackQuestions;
|
|
|
|
return questionsList.map((q) => {
|
|
const rawOptions =
|
|
q.options && q.options.length > 0
|
|
? q.options
|
|
: locale === "fa"
|
|
? ["بله", "به اندازه کافی واضح نیست", "نه"]
|
|
: ["Yes", "Not clear enough", "No"];
|
|
const mappedOptions = rawOptions.map((optText, idx) => ({
|
|
label: optText,
|
|
value: idx === 0 ? "A" : idx === 1 ? "B" : "C",
|
|
}));
|
|
return {
|
|
id: q.question_number,
|
|
text: q.text,
|
|
options: mappedOptions,
|
|
};
|
|
});
|
|
}, [cattellQuery.data, locale]);
|
|
|
|
const glasserTestQuestions: TestQuestion[] = useMemo(() => {
|
|
const questionsList =
|
|
glasserQuery.data?.questions && glasserQuery.data.questions.length > 0
|
|
? glasserQuery.data.questions
|
|
: glasserFallbackQuestions;
|
|
|
|
const defaultGlasserOptions = [
|
|
{ label: locale === "fa" ? "خیلی کم (۱)" : "Very Low (1)", value: 1 },
|
|
{ label: locale === "fa" ? "کم (۲)" : "Low (2)", value: 2 },
|
|
{ label: locale === "fa" ? "متوسط (۳)" : "Moderate (3)", value: 3 },
|
|
{ label: locale === "fa" ? "زیاد (۴)" : "High (4)", value: 4 },
|
|
{ label: locale === "fa" ? "خیلی زیاد (۵)" : "Very High (5)", value: 5 },
|
|
];
|
|
|
|
return questionsList.map((q) => ({
|
|
id: q.question_number,
|
|
text: q.text,
|
|
info:
|
|
"factor" in q
|
|
? (q.factor as string)
|
|
: "factor_code" in q
|
|
? (q.factor_code as string)
|
|
: undefined,
|
|
options: defaultGlasserOptions,
|
|
}));
|
|
}, [glasserQuery.data, locale]);
|
|
|
|
const visibleQuestions = useMemo(() => {
|
|
if (!item) {
|
|
return [];
|
|
}
|
|
|
|
const hasDobQuestion = item.questions.some(
|
|
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
|
|
);
|
|
|
|
return item.questions
|
|
.filter((question) => {
|
|
if (
|
|
hasDobQuestion &&
|
|
(question.title === "Age" || question.title === "سن")
|
|
) {
|
|
return false;
|
|
}
|
|
return isQuestionVisibleForProfile(question, profileContext);
|
|
})
|
|
.map((question) => ({
|
|
...question,
|
|
required: isQuestionRequiredForProfile(question, profileContext),
|
|
}));
|
|
}, [item, profileContext]);
|
|
|
|
const requiredQuestionsCount = useMemo(
|
|
() => visibleQuestions.filter((q) => q.required).length,
|
|
[visibleQuestions],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isProfileLoading) {
|
|
return;
|
|
}
|
|
|
|
if (!item || isQuestionListItemVisibleForProfile(item, profileContext)) {
|
|
return;
|
|
}
|
|
|
|
router.replace(questionsListHref);
|
|
}, [isProfileLoading, item, profileContext, questionsListHref, router]);
|
|
|
|
if (isProfileLoading && item) {
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center bg-[#F7F1F0]">
|
|
<DotsLoader className="text-[#F2465F] scale-150" />
|
|
</main>
|
|
</>
|
|
);
|
|
} else if (
|
|
!item ||
|
|
!isQuestionListItemVisibleForProfile(item, profileContext)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (item && item.questions.length === 0) {
|
|
if (isTestStarted) {
|
|
const isQuestionsLoading = isCattellSlug
|
|
? cattellQuery.isLoading || cattellQuery.isFetching
|
|
: isGlasserSlug
|
|
? glasserQuery.isLoading || glasserQuery.isFetching
|
|
: false;
|
|
|
|
if (isQuestionsLoading) {
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center bg-[#F7F1F0]">
|
|
<DotsLoader className="text-[#F2465F] scale-150" />
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const activeTestQuestions = isCattellSlug
|
|
? cattellTestQuestions
|
|
: isGlasserSlug
|
|
? glasserTestQuestions
|
|
: [];
|
|
|
|
if (activeTestQuestions.length === 0) {
|
|
const isError = isCattellSlug
|
|
? cattellQuery.isError
|
|
: isGlasserSlug
|
|
? glasserQuery.isError
|
|
: false;
|
|
const refetch = isCattellSlug
|
|
? cattellQuery.refetch
|
|
: glasserQuery.refetch;
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center gap-4 bg-[#F7F1F0] px-6 text-center">
|
|
<p className="font-semibold text-[#1B1B1B]">
|
|
{isError
|
|
? locale === "fa"
|
|
? "خطا در دریافت سوالات از سرور. لطفاً از اتصال اینترنت یا ورود به حساب کاربری اطمینان حاصل کنید."
|
|
: "Failed to load questions from server. Please check your connection or login status."
|
|
: locale === "fa"
|
|
? "سوالاتی برای این آزمون یافت نشد."
|
|
: "No questions found for this test."}
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsTestStarted(false)}
|
|
className="rounded-xl bg-[#EFEFEF] px-4 py-2 text-sm font-semibold text-[#1B1B1B]"
|
|
>
|
|
{closeLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => refetch()}
|
|
className="rounded-xl bg-[#F2465F] px-4 py-2 text-sm font-semibold text-white shadow-md"
|
|
>
|
|
{locale === "fa" ? "تلاش مجدد" : "Retry"}
|
|
</button>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const handleTestFinish = async (
|
|
answers: Record<number, string | number>,
|
|
) => {
|
|
if (isCattellSlug) {
|
|
const responses = Object.entries(answers).map(([qNum, option]) => ({
|
|
question_number: Number(qNum),
|
|
option: String(option),
|
|
}));
|
|
try {
|
|
await submitCattellMutation.mutateAsync({ responses });
|
|
} catch {
|
|
// Ignore if already submitted or API returned error
|
|
}
|
|
try {
|
|
window.localStorage.setItem(
|
|
getQuestionStorageKey(item.slug),
|
|
JSON.stringify({ completed: true }),
|
|
);
|
|
} catch {}
|
|
} else if (isGlasserSlug) {
|
|
const responses = Object.entries(answers).map(([qNum, score]) => ({
|
|
question_number: Number(qNum),
|
|
score: Number(score),
|
|
}));
|
|
try {
|
|
await submitGlasserMutation.mutateAsync({ responses });
|
|
} catch {
|
|
// Ignore if already submitted
|
|
}
|
|
try {
|
|
window.localStorage.setItem(
|
|
getQuestionStorageKey(item.slug),
|
|
JSON.stringify({ completed: true }),
|
|
);
|
|
} catch {}
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
};
|
|
|
|
return (
|
|
<TestQuestionsFlow
|
|
title={item.title}
|
|
questions={activeTestQuestions}
|
|
closeLabel={closeLabel}
|
|
informationLabel={informationLabel}
|
|
onClose={() => setIsTestStarted(false)}
|
|
onFinish={handleTestFinish}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const bulletKey =
|
|
item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
|
|
const bullets = t.questions.testIntroBullets[bulletKey];
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader sticky={false} className="shrink-0">
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={item.title}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<TestIntroPage
|
|
title={item.title}
|
|
estimateTime={item.estimate}
|
|
description={t.questions.testIntroEstimateLabel}
|
|
bulletPoints={bullets}
|
|
disclaimerText={t.questions.testIntroDisclaimer}
|
|
startLabel={t.questions.testIntroStart}
|
|
onStart={() => {
|
|
setIsTestStarted(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const dobQuestion = visibleQuestions.find(
|
|
(question) => question.title === "Date of Birth",
|
|
);
|
|
const dobQuestionIndex = visibleQuestions.findIndex(
|
|
(question) => question.title === "Date of Birth",
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<AnswerPaceSheet
|
|
slug={item.slug}
|
|
title={title}
|
|
description={description}
|
|
continueLabel={continueLabel}
|
|
/>
|
|
|
|
<QuestionAnswersProvider slug={item.slug} questions={visibleQuestions}>
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader sticky={false} className="shrink-0">
|
|
<div className="flex items-center gap-4">
|
|
<QuestionExitNavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
exitHref={questionsListHref}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={item.title}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<QuestionFlowWrapper
|
|
visibleQuestions={visibleQuestions}
|
|
itemSlug={item.slug}
|
|
dobQuestion={dobQuestion}
|
|
dobQuestionIndex={dobQuestionIndex}
|
|
requiredQuestionsCount={requiredQuestionsCount}
|
|
continueLabel={continueLabel}
|
|
questionsListHref={questionsListHref}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</QuestionAnswersProvider>
|
|
</>
|
|
);
|
|
}
|